Erkunden Sie fortschrittliche Techniken für paralleles Datenabrufen in React mit Suspense, um die Anwendungsleistung und Benutzererfahrung zu verbessern. Lernen Sie Strategien zur Koordination mehrerer asynchroner Operationen und zur effektiven Handhabung von Ladestatus.
React Suspense Koordination: Paralleles Datenabrufen meistern
React Suspense hat die Art und Weise revolutioniert, wie wir mit asynchronen Operationen umgehen, insbesondere mit dem Abrufen von Daten. Es ermöglicht Komponenten, das Rendern zu "suspendieren", während auf das Laden von Daten gewartet wird, und bietet so eine deklarative Möglichkeit, Ladestatus zu verwalten. Wenn jedoch einzelne Datenabrufe einfach mit Suspense umschlossen werden, kann dies zu einem Wasserfalleffekt führen, bei dem ein Abruf abgeschlossen wird, bevor der nächste beginnt, was sich negativ auf die Leistung auswirkt. Dieser Blogbeitrag befasst sich mit fortgeschrittenen Strategien zur parallelen Koordination mehrerer Datenabrufe mit Suspense, um die Reaktionsfähigkeit Ihrer Anwendung zu optimieren und die Benutzererfahrung für ein globales Publikum zu verbessern.
Das Wasserfallproblem beim Abrufen von Daten verstehen
Stellen Sie sich ein Szenario vor, in dem Sie ein Benutzerprofil mit Name, Avatar und den letzten Aktivitäten anzeigen müssen. Wenn Sie jedes Datenelement sequenziell abrufen, sieht der Benutzer einen Lade-Spinner für den Namen, dann einen weiteren für den Avatar und schließlich einen für den Aktivitäts-Feed. Dieses sequenzielle Ladeschema erzeugt einen Wasserfalleffekt, der das Rendern des vollständigen Profils verzögert und die Benutzer frustriert. Für internationale Benutzer mit unterschiedlichen Netzwerkgeschwindigkeiten kann diese Verzögerung noch ausgeprägter sein.
Betrachten Sie diesen vereinfachten Code-Ausschnitt:
function UserProfile() {
const name = useName(); // Ruft den Benutzernamen ab
const avatar = useAvatar(name); // Ruft den Avatar basierend auf dem Namen ab
const activity = useActivity(name); // Ruft die Aktivität basierend auf dem Namen ab
return (
<div>
<h2>{name}</h2>
<img src={avatar} alt="User Avatar" />
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
</div>
);
}
In diesem Beispiel sind useAvatar und useActivity vom Ergebnis von useName abhängig. Dies erzeugt einen klaren Wasserfall – useAvatar und useActivity können erst mit dem Abrufen von Daten beginnen, wenn useName abgeschlossen ist. Dies ist ineffizient und ein häufiges Leistungs-Nadelöhr.
Strategien für paralleles Abrufen von Daten mit Suspense
Der Schlüssel zur Optimierung des Abrufens von Daten mit Suspense liegt darin, alle Datenanforderungen gleichzeitig zu initiieren. Hier sind einige Strategien, die Sie anwenden können:
1. Daten mit `React.preload` und Ressourcen vorladen
Eine der leistungsstärksten Techniken besteht darin, Daten vorzuladen, bevor die Komponente überhaupt gerendert wird. Dies beinhaltet das Erstellen einer "Ressource" (ein Objekt, das die Datenabruf-Promise kapselt) und das Vorabrufen der Daten. `React.preload` hilft dabei. Bis die Komponente die Daten benötigt, sind sie bereits verfügbar, wodurch der Ladestatus fast vollständig entfällt.
Betrachten Sie eine Ressource zum Abrufen eines Produkts:
const createProductResource = (productId) => {
let promise;
let product;
let error;
const suspender = new Promise((resolve, reject) => {
promise = fetch(`/api/products/${productId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
product = data;
resolve();
})
.catch(e => {
error = e;
reject(e);
});
});
return {
read() {
if (error) {
throw error;
}
if (product) {
return product;
}
throw suspender;
},
};
};
// Verwendung:
const productResource = createProductResource(123);
function ProductDetails() {
const product = productResource.read();
return (<div>{product.name}</div>);
}
Jetzt können Sie diese Ressource vorladen, bevor die Komponente ProductDetails gerendert wird. Zum Beispiel während Routenübergängen oder beim Hovern.
React.preload(productResource);
Dies stellt sicher, dass die Daten wahrscheinlich verfügbar sind, wenn die Komponente ProductDetails sie benötigt, wodurch der Ladestatus minimiert oder beseitigt wird.
2. Verwenden von `Promise.all` für gleichzeitiges Abrufen von Daten
Ein weiterer einfacher und effektiver Ansatz ist die Verwendung von Promise.all, um alle Datenabrufe gleichzeitig innerhalb einer einzigen Suspense-Grenze zu initiieren. Dies funktioniert gut, wenn die Datenabhängigkeiten im Voraus bekannt sind.
Besuchen wir das Benutzerprofilbeispiel erneut. Anstatt Daten sequenziell abzurufen, können wir den Namen, den Avatar und den Aktivitäts-Feed gleichzeitig abrufen:
import { useState, useEffect, Suspense } from 'react';
async function fetchName() {
// API-Aufruf simulieren
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// API-Aufruf simulieren
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// API-Aufruf simulieren
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Ein Foto gepostet' },
{ id: 2, text: 'Profil aktualisiert' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
function Name() {
const name = useSuspense(fetchName());
return <h2>{name}</h2>;
}
function Avatar({ name }) {
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="User Avatar" />;
}
function Activity({ name }) {
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const name = useSuspense(fetchName());
return (
<div>
<Suspense fallback=<div>Avatar wird geladen...</div>>
<Avatar name={name} />
</Suspense>
<Suspense fallback=<div>Aktivität wird geladen...</div>>
<Activity name={name} />
</Suspense>
</div>
);
}
export default UserProfile;
Wenn jedoch sowohl `Avatar` als auch `Activity` auch von `fetchName` abhängig sind, aber innerhalb separater Suspense-Grenzen gerendert werden, können Sie die `fetchName`-Promise in das übergeordnete Element verschieben und über React Context bereitstellen.
import React, { createContext, useContext, useState, useEffect, Suspense } from 'react';
async function fetchName() {
// API-Aufruf simulieren
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// API-Aufruf simulieren
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// API-Aufruf simulieren
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Ein Foto gepostet' },
{ id: 2, text: 'Profil aktualisiert' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
const NamePromiseContext = createContext(null);
function Avatar() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="User Avatar" />;
}
function Activity() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const namePromise = fetchName();
return (
<NamePromiseContext.Provider value={namePromise}>
<Suspense fallback=<div>Avatar wird geladen...</div>>
<Avatar />
</Suspense>
<Suspense fallback=<div>Aktivität wird geladen...</div>>
<Activity />
</Suspense>
</NamePromiseContext.Provider>
);
}
export default UserProfile;
3. Verwenden eines benutzerdefinierten Hooks zur Verwaltung paralleler Abrufe
Für komplexere Szenarien mit potenziell bedingten Datenabhängigkeiten können Sie einen benutzerdefinierten Hook erstellen, um das parallele Abrufen von Daten zu verwalten und eine Ressource zurückzugeben, die Suspense verwenden kann.
import { useState, useEffect, useRef } from 'react';
function useParallelData(fetchFunctions) {
const [resource, setResource] = useState(null);
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
const promises = fetchFunctions.map(fn => fn());
const suspender = Promise.all(promises).then(
(results) => {
if (mounted.current) {
setResource({ status: 'success', value: results });
}
},
(error) => {
if (mounted.current) {
setResource({ status: 'error', value: error });
}
}
);
setResource({
status: 'pending',
value: suspender,
});
return () => {
mounted.current = false;
};
}, [fetchFunctions]);
const read = () => {
if (!resource) {
throw new Error('Ressource noch nicht initialisiert');
}
if (resource.status === 'pending') {
throw resource.value;
}
if (resource.status === 'error') {
throw resource.value;
}
return resource.value;
};
return { read };
}
// Beispielhafte Verwendung:
async function fetchUserData(userId) {
// API-Aufruf simulieren
await new Promise(resolve => setTimeout(resolve, 300));
return { id: userId, name: 'Benutzer ' + userId };
}
async function fetchUserPosts(userId) {
// API-Aufruf simulieren
await new Promise(resolve => setTimeout(resolve, 500));
return [{ id: 1, title: 'Beitrag 1' }, { id: 2, title: 'Beitrag 2' }];
}
function UserProfile({ userId }) {
const { read } = useParallelData([
() => fetchUserData(userId),
() => fetchUserPosts(userId),
]);
const [userData, userPosts] = read();
return (
<div>
<h2>{userData.name}</h2>
<ul>
{userPosts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}
function App() {
return (
<Suspense fallback=<div>Benutzerdaten werden geladen...</div>>
<UserProfile userId={123} />
</Suspense>
);
}
export default App;
Dieser Ansatz kapselt die Komplexität der Verwaltung der Promises und Ladestatus innerhalb des Hooks, wodurch der Komponentencode sauberer wird und sich stärker auf das Rendern der Daten konzentriert.
4. Selektive Hydrierung mit Streaming Server Rendering
Für serverseitig gerenderte Anwendungen führt React 18 selektive Hydrierung mit Streaming Server Rendering ein. Dies ermöglicht es Ihnen, HTML in Chunks an den Client zu senden, sobald es auf dem Server verfügbar ist. Sie können langsam ladende Komponenten mit <Suspense>-Grenzen umschließen, sodass der Rest der Seite interaktiv wird, während die langsamen Komponenten noch auf dem Server geladen werden. Dies verbessert die wahrgenommene Leistung erheblich, insbesondere für Benutzer mit langsamen Netzwerkverbindungen oder Geräten.
Stellen Sie sich ein Szenario vor, in dem eine Nachrichtenwebsite Artikel aus verschiedenen Regionen der Welt (z. B. Asien, Europa, Amerika) anzeigen muss. Einige Datenquellen sind möglicherweise langsamer als andere. Die selektive Hydrierung ermöglicht das Anzeigen von Artikeln aus schnelleren Regionen zuerst, während Artikel aus langsameren Regionen noch geladen werden, wodurch verhindert wird, dass die gesamte Seite blockiert wird.
Fehler und Ladestatus behandeln
Während Suspense die Verwaltung des Ladestatus vereinfacht, bleibt die Fehlerbehandlung von entscheidender Bedeutung. Fehlergrenzen (mit der componentDidCatch-Lifecycle-Methode oder dem useErrorBoundary-Hook aus Bibliotheken wie `react-error-boundary`) ermöglichen es Ihnen, Fehler, die beim Abrufen oder Rendern von Daten auftreten, elegant zu behandeln. Diese Fehlergrenzen sollten strategisch platziert werden, um Fehler innerhalb bestimmter Suspense-Grenzen abzufangen und zu verhindern, dass die gesamte Anwendung abstürzt.
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function MyComponent() {
// ... ruft Daten ab, die möglicherweise Fehler verursachen
}
function App() {
return (
<ErrorBoundary fallback={<div>Etwas ist schief gelaufen!</div>}>
<Suspense fallback={<div>Laden...</div>}>
<MyComponent />
</Suspense>
</ErrorBoundary>
);
}
Denken Sie daran, informative und benutzerfreundliche Fallback-UI für Lade- und Fehlerzustände bereitzustellen. Dies ist besonders wichtig für internationale Benutzer, die möglicherweise langsamere Netzwerkgeschwindigkeiten oder regionale Dienstausfälle erleben.
Best Practices zur Optimierung des Abrufens von Daten mit Suspense
- Kritische Daten identifizieren und priorisieren: Bestimmen Sie, welche Daten für das anfängliche Rendern Ihrer Anwendung unerlässlich sind, und priorisieren Sie das Abrufen dieser Daten zuerst.
- Daten wenn möglich vorladen: Verwenden Sie `React.preload` und Ressourcen, um Daten vorzuladen, bevor Komponenten sie benötigen, wodurch Ladestatus minimiert werden.
- Daten gleichzeitig abrufen: Verwenden Sie `Promise.all` oder benutzerdefinierte Hooks, um mehrere Datenabrufe parallel zu initiieren.
- API-Endpunkte optimieren: Stellen Sie sicher, dass Ihre API-Endpunkte für die Leistung optimiert sind, wodurch die Latenz und die Nutzlastgröße minimiert werden. Erwägen Sie die Verwendung von Techniken wie GraphQL, um nur die Daten abzurufen, die Sie benötigen.
- Caching implementieren: Cachen Sie häufig aufgerufene Daten, um die Anzahl der API-Anforderungen zu reduzieren. Erwägen Sie die Verwendung von Bibliotheken wie `swr` oder `react-query` für robuste Caching-Funktionen.
- Code-Splitting verwenden: Teilen Sie Ihre Anwendung in kleinere Chunks auf, um die anfängliche Ladezeit zu verkürzen. Kombinieren Sie Code-Splitting mit Suspense, um verschiedene Teile Ihrer Anwendung progressiv zu laden und zu rendern.
- Leistung überwachen: Überwachen Sie regelmäßig die Leistung Ihrer Anwendung mit Tools wie Lighthouse oder WebPageTest, um Leistungsengpässe zu identifizieren und zu beheben.
- Fehler elegant behandeln: Implementieren Sie Fehlergrenzen, um Fehler beim Abrufen und Rendern von Daten abzufangen und Benutzern informative Fehlermeldungen bereitzustellen.
- Server-Side Rendering (SSR) in Betracht ziehen: Erwägen Sie aus SEO- und Leistungsgründen die Verwendung von SSR mit Streaming und selektiver Hydrierung, um ein schnelleres anfängliches Erlebnis zu bieten.
Fazit
React Suspense bietet in Kombination mit Strategien für paralleles Abrufen von Daten ein leistungsstarkes Toolkit zum Erstellen reaktionsschneller und performanter Webanwendungen. Indem Sie das Wasserfallproblem verstehen und Techniken wie Vorladen, gleichzeitiges Abrufen mit Promise.all und benutzerdefinierte Hooks implementieren, können Sie die Benutzererfahrung erheblich verbessern. Denken Sie daran, Fehler elegant zu behandeln und die Leistung zu überwachen, um sicherzustellen, dass Ihre Anwendung für Benutzer weltweit optimiert bleibt. Während sich React weiterentwickelt, wird die Erkundung neuer Funktionen wie selektive Hydrierung mit Streaming Server Rendering Ihre Fähigkeit weiter verbessern, außergewöhnliche Benutzererlebnisse zu bieten, unabhängig von Standort oder Netzwerkbedingungen. Indem Sie diese Techniken anwenden, können Sie Anwendungen erstellen, die nicht nur funktional, sondern auch eine Freude für Ihr globales Publikum sind.
Dieser Blogbeitrag zielte darauf ab, einen umfassenden Überblick über parallele Datenabrufstrategien mit React Suspense zu geben. Wir hoffen, Sie fanden ihn informativ und hilfreich. Wir ermutigen Sie, mit diesen Techniken in Ihren eigenen Projekten zu experimentieren und Ihre Ergebnisse mit der Community zu teilen.